// 实现对数组响应式的拦截
const methods = ["shift", "unshift", "push", "pop", "splice", "reverse", "sort"]

const arrProtoType = Object.create(Array.prototype)

methods.forEach(method => {
  // 覆盖原有数组的方法
  arrProtoType[method] = function () {
    Array.prototype[method].call(this, ...arguments)
    // TODO 新增操作： 通知更新

  }
})

// 实现对象响应式拦截
const defineReactive = (obj, key, val) => {
  observe(val)
  // 创建dep实例和可以对应
  const dep = new Dep()
  return Object.defineProperty(obj, key, {
    get: () => {
      // 在这里做依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set: (v) => {
      if (v !== val) {
        observe(v)
        val = v
        dep.notify()
      }
    }
  })
}

// 完成数据嵌套结构
const observe = obj => {
  if (typeof obj !== "object" || obj === null) {
    return obj
  }
  if (Array.isArray(obj)) {
    // 覆盖原型，替换我们自己的
    obj.__proto__ = arrProtoType;
    Object.keys(obj).forEach(key => observe(obj[key]))
  }
  else {
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
  }
}
const proxy = vm => {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get: () => {
        return vm.$data[key]
      },
      set: (v) => {
        vm.$data[key] = v
      }
    })
  })
}

const regExp = /\{\{(.*)\}\}/;
class Vue {
  constructor(options) {
    // 1、对选项进行保存
    this.$options = options
    this.$data = options.data

    // 2、对data进行响应式处理
    observe(this.$data)

    // 2.5 代理
    proxy(this)

    // 3 编译
    new Compile(options.el, this)
  }
}

class Compile {
  constructor(el, vm) {
    // 1、首先保存下Vue的实例，后续会调用
    this.$vm = vm
    // 编译模板树
    this.compile(document.querySelector(el))
  }

  compile(el) {
    // 遍历el
    // 判断el的子元素类型
    el.childNodes.forEach(node => {
      if (node.nodeType === 1) {
        // 代表节点为元素
        this.compileElement(node)
        // 元素需要递归不然就看不到元素内不得值，只能看到当前元素的标签
        if (node.childNodes.length) {
          this.compile(node)
        }
      } else if (this.isInter(node)) {
        // 插值文本
        this.compileText(node)
      }
    })
  }

  // 统一做初始化和更新处理
  update(node, exp, dir) {
    // 初始化
    const fn = this[dir + "Updater"];

    fn && fn(node, this.$vm[exp])
    // 更新
    new Watcher(this.$vm, exp, function (val) {
      fn && fn(node, val)
    })
  }

  compileElement(node) {
    // 获取当前元素的所有属性，并判断他们是不是动态的
    const nodeAttrs = node.attributes

    Array.from(nodeAttrs).forEach(attr => {
      const attrName = attr.name;
      const exp = attr.value // 指令的内容
      // 判断是指令或者是事件是否是动态的
      if (attrName.startsWith("v-")) {
        const dir = attrName.substring(2)
        // 判断this实力上是否存在dir函数如果存在则调用
        this[dir] && this[dir](node, exp)
      }
      // 事件的处理
      if (this.isEvent(attrName)) {
        const dir = attrName.substring(1) // 事件名称
        // 事件监听
        this.eventHandler(node, exp, dir)
      }
    })
  }

  // 解析插值文本
  compileText(node) {
    const regexp = regExp.exec(node.textContent)
    this.update(node, regexp[1], "text")
    // node.textContent = this.$vm[regexp[1]]
  }

  textUpdater(node, val) {
    node.textContent = val
  }
  text(node, exp) {
    this.update(node, exp, "text")
    // node.textContent = this.$vm[exp]
  }
  htmlUpdater(node, val) {
    node.innerHTML = val
  }
  html(node, exp) {
    this.update(node, exp, "html")
    // node.innerHTML = this.$vm[exp]
  }
  modelUpdater(node, val) {
    // 只考虑大部分情况
    node.value = val
  }
  model(node, exp) {
    // update只负责赋值
    this.update(node, exp, "model")

    // 监听节点事件
    node.addEventListener(node.tagName.toLowerCase(), e => {
      // 对原数据进行反向赋值
      this.$vm[exp] = e.target.value
    })
  }

  // {{xxoo}}
  isInter(node) {
    return node.nodeType === 3 && regExp.test(node.textContent)
  }

  isEvent(dir) {
    return dir.startsWith("@")
  }

  eventHandler(node, exp, dir) {

    const methods = this.$vm.$options.methods

    const fn = methods && methods[exp]
    // 需要修改fn函数的this指向为当前的this.$vm
    node.addEventListener(dir, fn.bind(this.$vm))
  }
}

// 负责具体更新的Watcher
class Watcher {
  constructor(vm, key, updataFn) {
    this.vm = vm
    this.key = key
    this.updataFn = updataFn
    // 读取vm[key]触发依赖收集

    Dep.target = this
    vm[key]
    Dep.target = null
  }
  updata() {
    this.updataFn.call(this.vm, this.vm[this.key])
  }
}

class Dep {
  constructor() {
    this.desp = []
  }

  addDep(dep) {
    this.desp.push(dep)
  }
  notify() {
    this.desp.forEach(dep => dep.updata())
  }
}